פרק 3 רקורסיה רקורסיה נכתב ע"י רן רובינשטיין עודכן ע"י איתי שרון רקורסיה הינה שיטה לתכנון אלגוריתמים, שבה הפתרון לקלט כלשהו מתקבל על ידי פתרון אותה הבעיה בדיוק על קלט פשוט יותר, והרחבת פתרון זה לאחר מכן לפתרון עבור הקלט המורכב. באופן מעשי, פונקציה רקורסיבית הינה פונקציה ()f שתוך כדי ריצתה גורמת לקריאה חוזרת לעצמה. ברקורסיה ישירה הפונקציה ()f מבצעת קריאה מפורשת לעצמה. ברקורסיה עקיפה הפונקציה ()f קוראת לפונקציה אחרת ()g, וזו קוראת בשלב מאוחר יותר ל-() f. - - רקורסיות פשוטות: חישוב עצרת רקורסיה אלגוריתם רקורסיבי מורכב משני אלמנטים: צעד המעבר: זהו האופן בו מפרקים את הבעיה הנתונה לתתי-בעיות זהות אך פשוטות יותר, ולאחר מכן מרחיבים את הפתרונות שלהן לפתרון מלא של הבעיה המקורית. מקרה הבסיס: זהו אופן הפתרון של הבעיה (או הבעיות) בקצה פשוטות", "הכי אלו נחשבות הבעיות בעיות הרקורסיבית; השרשרת ואותן לא מפרקים יותר. וידוא נכונות של אלגוריתם רקורסיבי דורש: וידוא הנכונות של שני חלקי הרקורסיה (צעד המעבר ומקרה הבסיס). וידוא שכל שרשרת קריאות רקורסיביות אכן מסתיימת באחד ממקרי הבסיס. 3 usiged log factorial(usiged it ) if ( 0) retur ; retur * factorial(-); חישוב פעולת העצרת! באמצעות רקורסיה: מקרה הבסיס הוא 0, שעבורו התוצאה ידועה. צעד המעבר מצמצם את הבעיה מ-.!(-)! ל- -, באמצעות הנוסחה קל לוודא שמכל שמתחילים, תמיד מגיעים בסוף השרשרת למקרה הבסיס 0, כיוון שאנו מקטינים בכל קריאה את ב-. 4 תמונת המחסנית ב-() factorial x factorial(); factorial() if (0) retur ; retur * factorial(-); factorial() if (0) retur ; retur * factorial(-); factorial(0) if (0) retur ; retur * factorial(-); factorial() factorial() factorial() factorial(0) factorial() factorial() 5 רקורסיות פשוטות: פיבונאצ'י חישוב סדרת פיבונאצ'י באמצעות רקורסיה: usiged log fiboacci(usiged it ) if ( 0) retur 0; if ( ) retur ; retur fiboacci(-) + fiboacci(-); במקרה זה צעד המעבר מורכב משתי קריאות רקורסיביות. שימו לב שדרושים לנו כאן שני מקרי בסיס (עבור 0 וגם עבור ), כיוון שהצעד הרקורסיבי שכתבנו מוגדר רק ל- -ים החל מ-. כיוון שצעד הרקורסיה כאן איננו פשוט כמו במקרה של עצרת, עלינו לוודא בקפדנות שלכל שנתחיל ממנו, השרשרת אכן תסתיים תמיד באחד ממקרי הבסיס. 6
תמונת הקריאות ב-() fiboacci הגרף הבא מתאר את שרשרת הקריאות הרקורסיביות שמתבצעות עבור 5. כפי שניתן לראות, מספר הקריאות גדול מאוד ונעשים חישובים כפולים רבים. fiboacci(5) fiboacci(4) fiboacci(3) fiboacci(3) fiboacci() fiboacci() fiboacci() fiboacci() fiboacci() fiboacci() fiboacci(0) fiboacci() fiboacci(0) מספר הקריאות ב-() fiboacci בגלל העבודה הכפולה, מספר הקריאות הרקורסיביות בפונקציה זו גדל מהר מאוד ביחס ל-. שימו לב למספר הקריאות המבוצעות עבור -ים שונים! זמן החישוב (מחשב (P-IV מספר הקריאות הרקורסיביות 0 0 30 40 50 55 60 77,89,69,537 33,60,8 40,730,0,47 45,70,867,433 5,009,46,563,9 0.0 שניות 0.0 שניות 0. שניות 3.8 שניות,864 שניות 3. דקות 0,685 שניות 5.7 שעות 9,4 שניות.6 ימים fiboacci() fiboacci(0) מסקנה: בדוגמה זו, למרות הבהירות והפשטות של הקוד שכתבנו, לא ניתן להשתמש ברקורסיה כפי שמימשנו אותה, בגלל חוסר יעילות. 8 7 רקורסיות פשוטות: gcd() גם את gcd() ניתן לכתוב בצורה קומפקטית בעזרת רקורסיה: it gcd(it m, it ) if (0) retur m; retur gcd(, m%); פונקציה רקורסיבית נכתוב מערך a[] שברצוננו למיין. נתון :bubble sort את המיון לפי שמבצעת אין צורך לבצע דבר. עבור מערך בגודל הבסיס: מקרה. צעד המעבר, עבור מערך בגודל <:. במקרה זה לרקורסיה יתרון ברור: היא (כמעט) לא פחות יעילה מהגרסה עם הלולאה, והקוד הרבה יותר ברור. דוגמאות נוספות לרקורסיות פשוטות (נסו בעצמכם!): חישוב החזקה x עבור טבעי, חישוב סכום של סדרה חשבונית והנדסית, מציאת איבר מקסימאלי במערך וכן הלאה. בכל הדוגמאות הללו בדרך כלל קל לכתוב את הפונקציה גם ללא רקורסיה, תוך שימוש בלולאה. 9 Bubble Sort מימוש רקורסיבי רקורסיבית את המערך החל מהמקום השני בו. ממיינים "מבעבעים" את האיבר הראשון למקומו הנכון. a 4 37 37 4 6 56 56 6 0 45 a+ Bubble Sort מימוש רקורסיבי נתון מערך a[] שברצוננו למיין. נוכל לכתוב פונקציה רקורסיבית שמממשת bubble sort באופן פשוט למדי: void bubble_sort(it a[], it ) it i; if ( < ) retur; bubble_sort(a+, -); for(i0; (i<-) && (a[i]>a[i+]); i++) swap(a+i, a+i+); הערה: הפונקציה swap() מקבלת שני מצביעים ומחליפה את תוכנם. Bubble Sort מימוש רקורסיבי void bubble_sort(it a[], it ) it i0; if ( < ) retur; 45 37 a[] 4 6 56 a+ 45 4 37 56 6 4 37 45 56 6 bubble_sort(a+, -); while(a[i] > a[i+]) swap(a+i, a+i+); if ((++i) -) break;
נתכנת פונקציה שמציירת משולש בגובה שורות: האלגוריתם הרקורסיבי יהיה זה: הדפס משולש בגובה -.. הוסף שורת כוכביות באורך.. * * * * void draw_triagle(it ) it i; if ( < 0) retur; draw_triagle(-); for (i0; i<; ++i) putchar('*'); putchar('\'); מפת הקריאות בהדפסת משולש x draw_triagle(3); draw_triagle(3) draw_trigle(); draw_triagle() draw_triagle(); draw_triagle() draw_triagle(0); draw_triagle(0) if (0) retur; 3 * 3 * 4 3 מפת הקריאות בהדפסת משולש הפוך ומה אם היינו רוצים את המשולש הפוך? נשתמש באלגוריתם הרקורסיבי הבא: הדפס שורת כוכביות באורך.. הדפס משולש הפוך בגובה -.. * * * * void draw_triagle(it ) it i; if ( < 0) retur; for (i0; i<; ++i) putchar('*'); putchar('\'); draw_triagle(-); x draw_triagle(3); draw_triagle(3) draw_triagle(); draw_triagle() draw_triagle() draw_triagle() draw_triagle(0) draw_triagle(0) if (0) retur; * 3 3 * 6 5 שתי התוכניות מדגימות את המקרה הפשוט ביותר של רקורסיה רקורסיה ליניארית. בסוג זה של רקורסיה הפונקציה הרקורסיבית קוראת לעצמה פעם אחת ויחידה, וכך נוצרת שרשרת ליניארית של קריאות רקורסיביות לעומק. עם זאת, יש הבדל עקרוני אחד בין שתי התוכניות: בתוכנית הראשונה מתבצעת ראשית קריאה רקורסיבית לעומק, שבה לא מתבצע דבר למעשה. כל פעולת הציור מתבצעת בשלב ה"גלגול לאחור" של הרקורסיה. בתוכנית השנייה כל פעולת הציור נעשית בזמן "פרישת" הרקורסיה, כלומר תוך כדי הכניסה אליה. בשלב העלייה חזרה לא מתבצע דבר. ברקורסיה ליניארית מבחינים לפיכך בין שני חלקים של הפונקציה: החלק שלפני הקריאה הרקורסיבית מתבצע תוך כדי פרישת הרקורסיה. זהו "ראש" הרקורסיה. החלק שאחרי הקריאה הרקורסיבית מתבצע בזמן הגלגול בחזרה של הרקורסיה. זהו "זנב" הרקורסיה. מה יקרה אם נשלב בין שני החלקים, כלומר נכתוב פונקציה שבה מציירים שורת כוכביות גם לפני וגם אחרי הקריאה הרקורסיבית? 8 7
בדוגמה הבאה מופיע הקוד של הפונקציה המשולבת. התוצאה היא מעין דגלון העשוי משני משולשים הפוכים. אנו יכולים לתאר את הפונקציה שכתבנו במונחים של רקורסיה סטנדרטית. למעשה, האלגוריתם הרקורסיבי שמימשנו הוא: קוד מקדים קוד מאסף void draw_flag(it ) it i; if ( < 0) retur; for (i0; i<; ++i) putchar('*'); putchar('\'); draw_flag(-); for (i0; i<; ++i) putchar('*'); putchar('\'); * * * * * * * * הדפס שורת כוכביות באורך.. צייר דגלון בגודל -.. שורת כוכביות באורך. הדפס.3 הפלט של הפונקציה נראה כך: נסו בעצמכם! פונקציה רקורסיבית שמציירת פירמידה מכוכביות: 0 9 רק בשביל השעשוע... נשפר את מראה הדגלון בכך שנצייר את המשולש העליון רחב יותר, ואת המשולש התחתון קצר יותר: עבור 0, הפלט של פונקציה זו נראה כך: המשולש רחב פי 4 void draw_ice_flag(it ) it i; if ( < 0) retur; for (i0; i<; ++i) pritf(""); putchar('\'); draw_ice_flag(-); המשולש קצר פי if ( % ) retur; for (i0; i<; ++i) pritf("*"); putchar('\'); Ru! חישוב עצרת: רקורסיה ליניארית! דוגמאות לעצי קריאות עץ הקריאות של פונקציה רקורסיבית על מנת לחקור התנהגות של פונקציה רקורסיבית, הקריאות שלה. נוח להתבונן בעץ עץ הקריאות של פונקציה רקורסיבית הוא עץ שבו כל קודקוד מייצג את אחת הקריאות לפונקציה. השורש הוא הקריאה הראשונה לפונקציה (כלומר כשקוראים לפונקציה מחוץ לה), והבנים של כל קודקוד הם הקריאות הרקורסיביות שהפונקציה מבצעת במהלך ריצתה. העלים בעץ הקריאות מתאימים למקרי הבסיס של הרקורסיה, כיוון שהם מייצגים ריצות של הפונקציה שאינן מבצעות קריאות רקורסיביות. 3 usiged log factorial(usiged it ) if ( 0) retur ; retur * factorial(-); 4 factorial(4) factorial(3) factorial() factorial() factorial(0)
סיבוכיות של אלגוריתמים רקורסיביים דוגמאות לעצי קריאות usiged log fiboacci(usiged it ) if ( 0) retur 0; if ( ) retur ; retur fiboacci(-) + fiboacci(-); פיבונאצ'י: סיבוכיות זמן: קשורה למספר הכולל של קריאות רקורסיביות. סיבוכיות הזמן היא סך כל הזמן הדרוש לפונקציה, והוא שווה לסכום הזמן שדורשות כל הקריאות הרקורסיביות יחד. fiboacci(5) fiboacci(4) fiboacci(3) fiboacci(3) fiboacci() fiboacci() fiboacci() fiboacci() fiboacci() fiboacci() fiboacci(0) fiboacci() fiboacci(0) במקרה הפשוט, כל קריאה רקורסיבית מתבצעת בזמן קבוע ()Θ. במקרה זה הזמן הכולל הוא פשוט (מס' הקריאות הרקורסיביות) Θ. במקרה הכללי צריך להתבונן בעץ הקריאות של הפונקציה הרקורסיבית ולסכום את הזמנים הדרושים לכל הקריאות בעץ. fiboacci() fiboacci(0) 6 5 סיבוכיות של אלגוריתמים רקורסיביים סיבוכיות מקום: קשורה לעומק המקסימאלי של הרקורסיה (המספר המקסימאלי של קריאות רקורסיביות שמתקיימות בו זמנית על המחסנית). סיבוכיות המקום היא כמות הזיכרון המקסימאלית שהפונקציה צורכת במהלך ריצתה. כל כניסה רקורסיבית דורשת הקצאת מקום נוסף במחסנית, ואילו כל יציאה מהרקורסיה מפנה זיכרון זה. לכן כמות הזיכרון המקסימאלית שדורשת הפונקציה מתקבלת בדרך כלל כאשר אנו נמצאים בעומק המקסימאלי של הרקורסיה. סיבוכיות של פיבונאצ'י סיבוכיות זמן: כל קריאה לפונקציה דורשת מספר קבוע של פעולות, ולכן זמן הריצה הכולל הוא פשוט (מספר הקריאות הרקורסיביות) Θ, שזה גם (מספר הקודקודים בעץ הקריאות) Θ. סיבוכיות מקום: כל קריאה רקורסיבית צורכת כמות קבועה של זיכרון. לכן, השלב בו הכי הרבה זיכרון תפוש מתקבל כאשר אנו נמצאים בעומק המקסימאלי של הרקורסיה במצב זה יש הכי הרבה קריאות על המחסנית. במקרה הפשוט, כל קריאה רקורסיבית דורשת זיכרון קבוע ()Θ. במקרה זה הזיכרון הכולל הוא פשוט (עומק הרקורסיה המקסימאלי) Θ. במקרה הכללי יש לבחון את עץ הקריאות, למצוא את הקריאה בעומק המקסימאלי, ולסכום את כמות הזיכרון שתופשות כל הקריאות הרקורסיביות מהשורש ועד עליה. usiged log fiboacci(usiged it ) if ( 0) retur 0; if ( ) retur ; retur fiboacci(-) + fiboacci(-); 8 7 סיבוכיות של פיבונאצ'י סיבוכיות מקום: אם נתבונן בעץ הקריאות של הפונקציה, נבחין שהמסלול הארוך ביותר בעץ מגיע לעומק. לפיכך, המספר המקסימאלי של קריאות רקורסיביות שמתקיימות בו-זמנית על המחסנית הוא, וכיוון שכל קריאה רקורסיבית כזו תופשת כמות קבועה של זיכרון, אנו מקבלים שסיבוכיות הזיכרון הינה.Θ() fiboacci(-3) fiboacci() fiboacci(-) fiboacci(-) fiboacci(-) fiboacci(-3) fiboacci(-3) סיבוכיות של פיבונאצ'י סיבוכיות זמן: על מנת לקבל את סיבוכיות הזמן של פיבונאצ'י, עלינו לדעת את מספר הקודקודים בעץ הקריאות. הבעיה היא, שקשה לחשב את המספר המדויק של הקודקודים בעץ הזה. במקום זאת, נקבל חסם עליון וחסם תחתון על מספר בעץ. הקודקודים חסם עליון: אנו יודעים שהעומק המקסימאלי של העץ הוא. כעת, בעץ מלא בעומק יש - קודקודים, ואילו העץ שלנו איננו מלא ולכן יש בו לכל היותר - קודקודים. מכאן שמספר הקריאות הרקורסיביות הוא לכל היותר -, ולכן זמן הריצה חסום מלמעלה על ידי ).T()O( 30 9
סיבוכיות של פיבונאצ'י חסם תחתון: אם נתבונן שוב בעץ הקריאות, נראה שהעומק המינימאלי של העץ הוא /. עתה, בעץ מלא בעומק / יש / - קודקודים, ואילו העץ שלנו מכיל בתוכו את כל העץ הזה, ולכן יש בו לפחות / - קודקודים. מכאן שמספר הקריאות הרקורסיביות הוא לפחות / -, וזמן הריצה חסום מלמטה על ידי ) /.T()Ω( fiboacci() fiboacci(-) fiboacci(-) fiboacci(-3) fiboacci(-3) fiboacci(-4) fiboacci(-6) / סיבוכיות של factorial() פונקצית העצרת הינה רקורסיה ליניארית. נשים לב שכל קריאה רקורסיבית מקטינה את באחת, ולכן עומק הרקורסיה הוא.Θ() usiged log factorial(usiged ) if (0) retur ; retur * factorial(-); factorial(4) factorial(3) factorial() factorial() factorial(0) זמן ריצה: כל קריאה רקורסיבית דורשת מספר קבוע של פעולות, ומתבצעות סה"כ קריאות רקורסיביות, ולכן זמן הריצה הוא.Θ() זיכרון: כל קריאה רקורסיבית דורשת זיכרון בגודל קבוע (שאיננו תלוי ב- ), והמספר המקסימאלי של קריאות רקורסיביות שמתקיימות בו-זמנית הוא. לכן סיבוכיות הזיכרון היא Θ() גם כן. 3 3 סיבוכיות של חיפוש בינארי חיפוש בינארי ניתן למימוש כרקורסיה ליניארית. בכל קריאה רקורסיבית מקטינים את תחום החיפוש פי על פי האיבר האמצעי. it bisearch(it a[], it, it x) if ( < 0) retur -; if (a[/] x) retur /; if (a[/] > x) retur bisearch(a,/,x); else it pos bisearch(a+/+, -/-, x); if (pos -) retur -; retur pos + / + ; סיבוכיות של חיפוש בינארי במקרה של חיפוש בינארי, כל קריאה רקורסיבית מקטינה את פי, ולכן עומק הרקורסיה הוא.Θ(log()) זמן ריצה: בכל קריאה רקורסיבית מבוצע מספר קבוע של פעולות, ולכן זמן הריצה הוא.Θ(log()) דרישות זיכרון: כל קריאה רקורסיבית צורכת זיכרון בגודל קבוע, ולכן סיבוכיות הזיכרון אף היא.Θ(log()) 34 33 פיתוח טלסקופי של factorial() ניתוח סיבוכיות באמצעות פיתוח טלסקופי שיטת הפיתוח הטלסקופי הינה שיטה נוחה לניתוח הסיבוכיות של רקורסיות רבות.. נתחיל מכתיבת ביטוי לא מפורש ל-( T(, כזה שמסתמך על ידיעת ערכו של T עבור -ים קטנים יותר. את הביטוי ניתן לקבל ישירות מהקוד של הפונקציה הרקורסיבית: באופן כללי, טכניקת הפיתוח הטלסקופי מאפשרת לנתח זמני ריצה על סכימה שיטתית של זמני הריצה של כל הקריאות הרקורסיביות ידי שמתבצעות. ניתן להשתמש גם בווריאציה של השיטה לשם ניתוח סיבוכיות מקום, אך זה לרוב פחות נוח (אנו נראה דוגמה אחת לכך). הטכניקה תוסבר דרך מספר דוגמאות. אנו נתחיל מהמקרה הפשוט ביותר: ננתח (שוב...) את פונקצית העצרת. שימו לב לשלבי העבודה הם יחזרו על עצמם בהמשך. usiged log factorial(usiged it ) if ( 0) retur ; retur * factorial(-); במקרה שלנו, נקבל מקוד הפונקציה את הביטוי הבא, שמתאר את T() באופן לא מפורש (כאשר C הוא קבוע המציין את מספר הפעולות שמתבצעות בפונקציה ללא הקריאה הרקורסיבית): T T ( ) + C 36 35
פיתוח טלסקופי של factorial() במקום בביטוי שרשמנו. נקבל כי זמן הריצה עבור T ( ) T ( ) + C T T ( ) + C. כעת, נציב - (-) מקיים: נקבל: זה ניתן להציב בחזרה בביטוי ל-( T(. ערך ( ) T + C + C T ( ) + C פיתוח טלסקופי של factorial() נמשיך כך, ונקבל את הפיתוח הטלסקופי של :T() T T ( ) + C ( ) T + C + C T ( ) + C T ( 3) + 3C M T ( ) T ( 3) + C T ( k) + k C.3 באופן דומה נוכל להמשיך ולהציב עבור -, 3- וכן הלאה. כאשר השורה התחתונה היא השורה ה- k בפיתוח הטלסקופי. 38 37 פיתוח טלסקופי של factorial() 4. שרשרת הפיתוח נעצרת כאשר מגיעים למקרה הבסיס. שימו לב שעבור מקרה זה, הביטוי שרשמנו בתחילה ל- T() איננו תקף. במקרה של,factorial() מקרה הבסיס הוא 0, ולכן כאשר נגיע ל-( T(0 בפיתוח נעצור. ובכן, מתי נגיע ל-( T(0 בפיתוח הטלסקופי? לשם כך נתבונן בביטוי שחישבנו עבור השורה ה- k בפיתוח: T T ( k) + k C אנו רואים כי עבור ההצבה k (כלומר בשורה ה- של הפיתוח) נקבל בדיוק (0)T. נציב אם כך k בביטוי זה ונקבל: פיתוח טלסקופי של factorial() 5. זהו, סיימנו! (0)T הוא מספר קבוע, כיוון שהוא איננו תלוי ב-. לכן נוכל להחליפו בקבוע C (זהו למעשה מספר הפעולות הדרושות עבור הקלט 0), ואנו מקבלים את הביטוי המפורש ל-( T( : T ( ) T (0) + C C + C Θ T T (0) + C 40 39 פיתוח טלסקופי של Biary Search פיתוח טלסקופי של Biary Search נראה כעת כיצד ניתן לקבל את זמן הריצה של biary search גם כן באמצעות פיתוח טלסקופי. 3. נמשיך כך ונקבל את הפיתוח הטלסקופי של.T() אנו רוצים לקבל ביטוי לשורה ה- k בפיתוח, ולכן נרשום:. בכל איטרציה של הפונקציה הרקורסיבית, מתבצע מספר קבוע C של פעולות, וכן מתבצעת קריאה רקורסיבית עם קלט בגודל /. לפיכך, הוא: הלא מפורש ל-( T( הביטוי T T + C T T + C 4 הצבת / בביטוי זה נותנת: 4. ( ( 4 ) ) T( 4) C T 3C T T + C T + C + C + + 8 ( k) T + k C M 4
פיתוח טלסקופי של Biary Search 4. במקרה שלנו מקרה הבסיס הוא עבור מערך בגודל. מתי נגיע ל-( T(? ובכן, ניתן לראות שזה מתרחש כאשר, k שזה אומר.klog() שרצינו): (כפי ונקבל זאת בביטוי ל-( T(, נציב T T () + log C C + log C Θ(log) סיבוכיות של Bubble Sort ננתח כעת את הסיבוכיות של אלגוריתם המיון bubble sort שפגשנו בתחילת הפרק: void bubble_sort(it A[], it N) it i0; if (N < ) retur; bubble_sort(a+, N-); for(i0; (i<-) && (a[i]>a[i+]); i++) swap(a+i, a+i+); 44 43 סיבוכיות של Bubble Sort כל קריאה רקורסיבית מקטינה את במקרה זה הוא.Θ() ב-, ולכן עומק הרקורסיה סיבוכיות זיכרון: כל קריאה רקורסיבית דורשת זיכרון בגודל קבוע, ולכן סיבוכיות הזיכרון היא.Θ() זמן ריצה: לשם כך נבצע פיתוח טלסקופי של זמן הריצה. נשים לב שבכל איטרציה של הפונקציה הרקורסיבית מתבצעת קריאה רקורסיבית עם מערך בגודל -, וכן מתבצעות (במקרה הגרוע) עוד החלפות. לכן זמן הריצה מקיים: T T ( ) + C פיתוח טלסקופי של Bubble Sort מהפיתוח הטלסקופי נקבל במפורש את זמן הריצה: T T ( ) + C T ( ) + C( ) + C T ( 3) + C ( ) + C ( ) + C T ( k) + C( ( k )) + L+ C( ) + C M T (0) + C(+ + L+ ( ) + ) ( + ) C+ C Θ 46 45 ניתוח אלגוריתם Merge Sort ננתח כעת את הסיבוכיות של מיון,merge sort בגרסתו הרקורסיבית. נזכיר ראשית סקיצה של אלגוריתם merge sort הרקורסיבי: סיבוכיות הזמן של Merge Sort נשים לב שבכל פעם שנכנסים לתוך קריאה רקורסיבית, אורך המערך קטן פי. לכן, העומק המקסימאלי של הרקורסיה הוא.Θ(log()) merge_sort(a[n]) if (N < ) retur; allocate tmp[n]; merge_sort( A[0..N/] ); merge_sort( A[N/+..N-] ); tmp merge( A[0..N/], A[N/+..N-] ); memcpy(a tmp); זמן ריצה: נבצע כרגיל פיתוח טלסקופי עבור זמן הריצה. מתבצעות איטרציה של merge_sort() בכל שניתן להבחין, כפי שתי קריאות רקורסיבית, כל אחת עם מערך בגודל /. כמו כן מתבצעת גם פעולת,merge שדורשת עוד כ- פעולות. T T + C אנו מקבלים את הביטוי הבא: 48 47
פיתוח טלסקופי של Merge Sort פיתוח טלסקופי של Merge Sort הפיתוח הטלסקופי של ביטוי זה הינו: ( ( 4) ) T( 4) C T C T T + C T + C + C 4 + 8 + 3 8 M + kc k T k על מנת להגיע למקרה הבסיס, נקבל את התוצאה: עלינו להציב, k כלומר.klog() k T T k + kc T + C log C + Clog Θ( log) 50 49 סיבוכיות הזיכרון של Merge Sort מה לגבי סיבוכיות הזיכרון של?merge sort ובכן, גם במקרה זה ניתן לבצע פיתוח טלסקופי, אולם הגישה מעט שונה. נזכיר שבמקרה של סיבוכיות זיכרון, אנו מעוניינים בכמות הזיכרון המקסימאלית הנדרשת לשם ביצוע הפונקציה. הזיכרון התפוס גדלה כמות ריצת הפונקציה הרקורסיבית, במהלך וקטנה כל העת כאשר אנו נכנסים ויוצאים מקריאות רקורסיביות. מה שעלינו לעשות הוא לזהות את השלב ברקורסיה שבו כמות הזיכרון התפוס היא הגדולה ביותר. סיבוכיות הזיכרון של Merge Sort כפי שמייד נראה, הפיתוח הטלסקופי עבור סיבוכיות זיכרון משקף את פעולת המקסימיזציה הזו, על ידי החלפת פעולות החיבור שראינו בחישובי סיבוכיות הזמן עם פעולות מקסימום. כל קריאה רקורסיבית דורשת זיכרון להקצאת מערך tmp שגודלו, וכן זיכרון נוסף לצורך ביצוע הקריאות הרקורסיביות. נשים לב שלמרות שישנן שתי קריאות רקורסיביות בפונקצית המיון, ומפנה קודם הראשונה מסתיימת אלא אינן מתבצעות בו זמנית, הן את הזיכרון שהיא תפסה, ורק לאחר מכן השנייה מתחילה. לכן, הזיכרון המקסימאלי שיהיה תפוס במהלך ריצת הפונקציה הוא זה הדרוש לאחסון,tmp ועוד זה שנדרש על ידי הקריאה הרקורסיבית שתופסת יותר זיכרון מבין השתיים שמבוצעות. 5 5 סיבוכיות הזיכרון של Merge Sort נסמן את סיבוכיות הזיכרון עבור מערך באורך ב-( S(. לפיכך, כל אחת מן הקריאות הרקורסיביות דורשת זיכרון בגודל.S(/) הקצאת המערך tmp דורשת C ועצם הכניסה לפונקציה עוד C. נקבל את הביטוי הבא עבור :S() ( ( ) ) S C + C + max S, S במקרה שלנו שני הערכים בתוך ה- max שווים, ואנו מקבלים ביטוי פשוט ל-( S(. פיתוח טלסקופי של ביטוי זה נותן (בדקו!): ( ) S C + C + S Θ לסיום: היפוך מחרוזת נכתוב לסיום פונקציה strflip() שמקבלת מחרוזת ומחזירה אותה הפוכה (מהסוף להתחלה), אך מבצעת זאת רקורסיבית. נראה כאן שתי אלטרנטיבות, ונשווה את זמן הריצה שלהן: void strflip(char *str) if (*str 0) retur; strflip(str+); while (str[]! 0) swap(str,str+); str++; T T ( ) + C L Θ( ) 54 53
היפוך מחרוזת: אופציה שנייה הפתרון הקודם איננו יעיל במיוחד: הוא דורש זמן ריבועי ב-, כאשר אנו יודעים שניתן לעשות אותה הפעולה בזמן ליניארי ב-. הפתרון הבא יעיל יותר, ואולם הוא דורש שהפונקציה תקבל שני מצביעים: אחד לתו הראשון במחרוזת, והשני לתו האחרון בה (הכוונה לתו שלפני ה- ull ): void strflip(char *begi, char *ed) if (begi > ed) retur; swap(begi, ed); strflip(begi+, ed-); פונקציות מעטפת בעיה קטנה: הפתרון השני אמנם יעיל יותר, אך חתימת הפונקציה שכתבנו שונה מזו שאנו רוצים! ישנה טכניקה סטנדרטית לפתרון סוגיה זו: נכתוב פונקצית מעטפת ש"תעטוף" את הפונקציה הרקורסיבית שכתבנו. הרעיון הוא שהפונקציה שהמשתמש קורא לה בפועל תהיה פונקצית המעטפת, והיא תהייה בדיוק עם החתימה אותה אנו רוצים. במעשה, כל תפקידה של פונקציה זו הוא לקרוא לפונקציה הרקורסיבית, כאשר היא מספקת לה את כל הפרמטרים הנוספים אותם אנו רוצים "להסתיר" מהמשתמש. על מנת שהמשתמש לא יהיה מודע לכל זאת, ניתן לפונקצית המעטפת את השם,strflip() ואת שם הפונקציה הרקורסיבית שכתבנו נשנה ל-() strflip_aux. 56 55 היפוך מחרוזת: האופציה השנייה נקבל את צמד הפונקציות הבאות. שימו לב שלמעשה המשתמש יודע רק על קיומה של הפונקציה הראשונה (פונקצית המעטפת): void strflip(char *str) strflip_aux(str, str + strle(str)-); void strflip_aux(char *begi, char *ed) if (begi > ed) retur; swap(begi, ed); strflip_aux(begi+, ed-); T T ( ) + C L Θ 57